Skip to content

Conversation

@Sh0g0-1758
Copy link
Member

@Sh0g0-1758 Sh0g0-1758 commented Oct 7, 2025

This PR aims to add a pass that reduces the diff noise between two roughly semantically similar MLIR modules. We do this in a 2 step process. The first is instruction reordering. Now two semanitcally similar modules should have roughly the same type of side-effects and the same structure of control flow graph. So we collect operations (termed as output, which include operations with an IsTerminator trait, operations with a MemoryEffects::Write side effect, and call operation) and walk them top-down recursively so that for each operand, we bring their definition as close as possible to the using operation. The second is deterministic SSA naming. Simple linear naming won't work because the addition of any new operation in two roughly semantically similar modules would change the diff a lot. The naming scheme we have chosen derives from the one introduced in llvm-canon (refer to this talk for further details). There is one aspect of it that might need some discussion on. While naming initial operations, we collect outputFootprint as the distance of output operations from the beginning of the function. But this means that the addition of any redundant operation will change the name of all the inital operations which would pollute the diff a lot. Instead, perhaps we can simply add the number of output operations using that initial operation in the hash ?

Patch made in collaboration with @anant37289

@github-actions
Copy link

github-actions bot commented Oct 7, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@Sh0g0-1758 Sh0g0-1758 added the mlir label Oct 7, 2025
@Sh0g0-1758 Sh0g0-1758 self-assigned this Oct 7, 2025
@Sh0g0-1758 Sh0g0-1758 marked this pull request as ready for review October 10, 2025 14:27
@Sh0g0-1758 Sh0g0-1758 changed the title [WIP][mlir] Add Normalize pass [mlir] Add Normalize pass Oct 10, 2025
@Sh0g0-1758 Sh0g0-1758 requested a review from jpienaar October 10, 2025 14:41
Copy link
Member

@jpienaar jpienaar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you expand the PR desciption as well as pass and function comments to capture the goals, what's working etc?


uint64_t Hash = MagicHashConstant;

uint64_t opcodeHash = strHash(op->getName().getStringRef().str());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StringRef has hash_value , could that be used?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. iirc, set_fixed_execution_hash_seed has been removed in the latest tree which means llvm::hash_value currently gives different hash in subsequent runs (unless you rebuild llvm with certain flags). I believe the user would want the SSA names to be consistent throughout runs.

if (op.hasTrait<OpTrait::IsTerminator>())
return true;

if (auto memOp = dyn_cast<MemoryEffectOpInterface>(&op)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you comment as to why?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check the updated description once and let me know if its still not clear ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment should be in the code too, not just description. But no it doesn't really explain why. It just says it is done. IRNormalizer.cpp makes it clearer as not to reorder around side-effecting operations.

Operands.push_back({Stream.str(), operand});
}

if (op->hasTrait<OpTrait::IsCommutative>()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commutative operations are naturally reordered while considering constants, would this be considered here too giving naming conventions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the constants would have been hashed during the nameAsInitialOperation call, so if the operation is commutative, the constant operands would get reordered accordingly.


// CHECK-LABEL: module {
// CHECK: func.func @infinte_loop(%[[ARG0:.*]]: memref<?xi32>, %[[ARG1:.*]]: i32) {
// CHECK: %vl15969$e5677$ = arith.constant 1 : i32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's up with these cryptic SSA variable names?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the point of llvm-canon, it gives everything predictable "canonical" names so when you compare two files textually, they don't differ because of SSA value numbers.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can refer to this talk to further understand the motivation behind llvm-canon.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, its useful for that but indeed terrible to read :) (I almost wonder if the diff tool post would be smart enough to reduce it to something smaller ... or perhaps give both files in to diffing so that names could be chosen with more care - e.g., take all names across both, rename across both s/%vl15390$funcArg1-vl15969$/%0/ etc)

private:
const uint64_t MagicHashConstant = 0x6acaa36bef8325c5ULL;
void
collectOutputOperations(Block &block,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the functions here should have documentation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you may have forgotten to upload the change.

I also like how LLVM's pass grouped these (llvm/lib/Transforms/Utils/IRNormalizer.cpp)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added the documentation above the definition of each function. Though I left 2-3 functions whose definitions were short or their functionality was intuitive from the name.

Copy link
Member

@ftynse ftynse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your patch! Please provide a clear commit description, explaining why you make changes, not only what they are: https://mlir.llvm.org/getting_started/Contributing/#commit-messages.

Two large items that will need addressing is (1) this does not belong to lib/Conversion because it is not a conversion and it will have to operate generally on all MLIR operations regardless of dialects and (2) we need to find a way of testing the desired goal of the tool which is minizing textual difference between files to "semantically meaningful" parts.

@anant37289
Copy link

anant37289 commented Oct 21, 2025

(1) this does not belong to lib/Conversion because it is not a conversion and it will have to operate generally on all MLIR operations regardless of dialects

Should we make a separate tool out of it or place it in some other directory like rewrite..?

(2) we need to find a way of testing the desired goal of the tool which is minizing textual difference between files to "semantically meaningful" parts.

We can try making 2 forms of the same initial code by applying 1-2 different optimizations and expect small diffs because of them being semantically similar as mentioned here for mir canon

Copy link
Member

@jpienaar jpienaar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any test with side-effecting ops

//===----------------------------------------------------------------------===//
// Normalize
//===----------------------------------------------------------------------===//
def Normalize : Pass<"normalize", "ModuleOp"> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it required to be a ModuleOp pass? Does it need to be at symbol table scope?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well we do need the top-level module operation to collect all the output operations and thus reorder/rename regular/initial operations if they have not been visited earlier by another output operation using them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite follow this. I may be missing what you are saying as I think this is true and could work even for functions/doesn't even need to be on top-level ops. But it does need to (correct me if I'm wrong) run sequentially due to rng update ordering.

visited.insert(op);

if (isOutput(*op)) {
func::FuncOp func = op->getParentOfType<func::FuncOp>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can avoid making this on FuncOp and just query the Block in which this operation is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we need the enclosing function to get different footprint for output operations at different depth. If we measure the footprint from block, then two output operations at same depth from their enclosing block would have same footprint.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true for a FuncOp too, but what is indeed different is that FuncOp is IsolatedFromAbove. Would that be sufficient?

My goal here is to not hardcode an assumption of 1 top level module with N functions underneath. But enable it to run even where there is another top level op and other region ops.

/// Helper method returning indices (distance from the beginning of the basic
/// block) of output operations using the given operation. Walks down the
/// def-use tree recursively
llvm::SetVector<int> NormalizePass::getOutputFootprint(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this used below? Seems to be for hashing, but not sure I followed the logic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the outputfootprint is used to differentiate between initial operations that have the same def but used by different output operations.


// CHECK-LABEL: module {
// CHECK: func.func @infinte_loop(%[[ARG0:.*]]: memref<?xi32>, %[[ARG1:.*]]: i32) {
// CHECK: %vl15969$e5677$ = arith.constant 1 : i32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, its useful for that but indeed terrible to read :) (I almost wonder if the diff tool post would be smart enough to reduce it to something smaller ... or perhaps give both files in to diffing so that names could be chosen with more care - e.g., take all names across both, rename across both s/%vl15390$funcArg1-vl15969$/%0/ etc)

%tmp41 = arith.xori %tmp40, %cneg1 : i32
%tmp42 = arith.addi %tmp39, %tmp41 : i32
%tmp43 = arith.addi %tmp42, %c0 : i32
%tmp44 = arith.muli %tmp43, %tmp40 : i32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of these seem like they can't be reordered due to creating a rather linear sequence here. How about doing a N node tree-link input structure here linearized, that way you could have both short and long term reorderings? (of course many other generated could work, goal is just to know there are many variants possible and given two variants see same output).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well this test was largely inspired by a similar test in llvm-canon pr : https://github.com/llvm/llvm-project/pull/113780/files#diff-25aa6a3cb4c2af6c07edea9fa7b12a1afefa1337b2bb1802cb4658a3a2fb6353 .

But I have modified our infinite-loop test into 2 variants with tree-like dependencies that will produce the same canonical form after the normalize pass reorders them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not seeing it, I see

    %tmp23 = arith.muli %tmp22, %tmp19 : i32
    %tmp24 = arith.xori %tmp23, %cneg1 : i32
    %tmp25 = arith.addi %tmp22, %tmp24 : i32
    %tmp26 = arith.addi %tmp25, %c0 : i32
    %tmp27 = arith.muli %tmp26, %tmp23 : i32
    %tmp28 = arith.xori %tmp27, %cneg1 : i32
    %tmp29 = arith.addi %tmp26, %tmp28 : i32
    %tmp30 = arith.addi %tmp29, %c0 : i32
...

and you have 24 depend on 23, 25 on 24. 26 on 25, 27 on 26, 28 on 27, 29 on 27, 30 on 29 - so here there is no reordering possible in this snippet.

@Sh0g0-1758 Sh0g0-1758 requested a review from jpienaar October 29, 2025 11:01
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just +1 to Alex's point that this isn't a Conversion in MLIR terminology (https://mlir.llvm.org/getting_started/Glossary/#conversion). Transforms could be better spot, although I was indeed also wondering if in Tools directory wouldn't be better as it is rather specific when one runs this.

%tmp41 = arith.xori %tmp40, %cneg1 : i32
%tmp42 = arith.addi %tmp39, %tmp41 : i32
%tmp43 = arith.addi %tmp42, %c0 : i32
%tmp44 = arith.muli %tmp43, %tmp40 : i32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not seeing it, I see

    %tmp23 = arith.muli %tmp22, %tmp19 : i32
    %tmp24 = arith.xori %tmp23, %cneg1 : i32
    %tmp25 = arith.addi %tmp22, %tmp24 : i32
    %tmp26 = arith.addi %tmp25, %c0 : i32
    %tmp27 = arith.muli %tmp26, %tmp23 : i32
    %tmp28 = arith.xori %tmp27, %cneg1 : i32
    %tmp29 = arith.addi %tmp26, %tmp28 : i32
    %tmp30 = arith.addi %tmp29, %c0 : i32
...

and you have 24 depend on 23, 25 on 24. 26 on 25, 27 on 26, 28 on 27, 29 on 27, 30 on 29 - so here there is no reordering possible in this snippet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you rerun the input through mlir-opt? (the space just feels off here).

return true;
}

if (auto call = dyn_cast<func::CallOp>(op))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've resolved the comment, but I still see this on func::CallOp

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants